使用alarm函数设置超时
void handler(int sig)
{
}
signal(SIGALRM, handler);
alarm(5);
int ret = read(fd, buf, sizeof(buf));
if (ret == -1 && errno == EINTR)
errno = ETIMEOUT;
else if (ret >= 0)
alarm(0);
EINTR为中断信号,如果read在5s内被SIGALRM信号中断而返回,则表示超时,否则未超时已读取到数据,取消闹钟。但这种方法不常用,因为有时可能在其他地方使用了alarm会造成混乱。
使用套接字选项SO_SNDTIMEO、SO_RCVTIMEO
struct timeval timeout = {3,0};
setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, (char *)&timeout, sizeof(struct timeval));
int ret = read(sock, buf, sizeof(buf));
if (ret == -1 && errno == EWOULDBLOCK)
errno = ETIMEOUT;
使用setsockopt 函数进行设置,但这种方法可移植性比较差,不是每种系统实现都有这些选项。
使用select 实现超时
read_timeout、write_timeout、accept_timeout、connect_timeout
四个函数封装
#include "sysutil.h"
/* read_timeout - 读超时检测函数,不含读操作
* fd:文件描述符
* wait_seconds:等待超时秒数, 如果为0表示不检测超时;
* 成功(未超时)返回0,失败返回-1,超时返回-1并且errno = ETIMEDOUT
*/
int read_timeout(int fd, unsigned int wait_seconds)
{
int ret = 0;
if (wait_seconds > 0)
{
fd_set read_fdset;
struct timeval timeout;
FD_ZERO(&read_fdset);
FD_SET(fd, &read_fdset);
timeout.tv_sec = wait_seconds;
timeout.tv_usec = 0;
do
{
ret = select(fd + 1, &read_fdset, NULL, NULL, &timeout); //select会阻塞直到检测到事件或者超时
// 如果select检测到可读事件发送,则此时调用read不会阻塞
}
while (ret < 0 && errno == EINTR);
if (ret == 0)
{
ret = -1;
errno = ETIMEDOUT;
}
else if (ret == 1)
return 0;
}
return ret;
}
/* write_timeout - 写超时检测函数,不含写操作
* fd:文件描述符
* wait_seconds:等待超时秒数, 如果为0表示不检测超时;
* 成功(未超时)返回0,失败返回-1,超时返回-1并且errno = ETIMEDOUT
*/
int write_timeout(int fd, unsigned int wait_seconds)
{
int ret = 0;
if (wait_seconds > 0)
{
fd_set write_fdset;
struct timeval timeout;
FD_ZERO(&write_fdset);
FD_SET(fd, &write_fdset);
timeout.tv_sec = wait_seconds;
timeout.tv_usec = 0;
do
{
ret = select(fd + 1, NULL, &write_fdset, NULL, &timeout);
}
while (ret < 0 && errno == EINTR);
if (ret == 0)
{
ret = -1;
errno = ETIMEDOUT;
}
else if (ret == 1)
return 0;
}
return ret;
}
/* accept_timeout - 带超时的accept
* fd: 套接字
* addr: 输出参数,返回对方地址
* wait_seconds: 等待超时秒数,如果为0表示正常模式
* 成功(未超时)返回已连接套接字,失败返回-1,超时返回-1并且errno = ETIMEDOUT
*/
int accept_timeout(int fd, struct sockaddr_in *addr, unsigned int wait_seconds)
{
int ret;
socklen_t addrlen = sizeof(struct sockaddr_in);
if (wait_seconds > 0)
{
fd_set accept_fdset;
struct timeval timeout;
FD_ZERO(&accept_fdset);
FD_SET(fd, &accept_fdset);
timeout.tv_sec = wait_seconds;
timeout.tv_usec = 0;
do
{
ret = select(fd + 1, &accept_fdset, NULL, NULL, &timeout);
}
while (ret < 0 && errno == EINTR);
if (ret == -1)
return -1;
else if (ret == 0)
{
errno = ETIMEDOUT;
return -1;
}
}
if (addr != NULL) //addr为传出参数
ret = accept(fd, (struct sockaddr *)addr, &addrlen);
else
ret = accept(fd, NULL, NULL);
if (ret == -1)
ERR_EXIT("accpet error");
return ret;
}
/* activate_nonblock - 设置IO为非阻塞模式
* fd: 文件描述符
*/
void activate_nonblock(int fd)
{
int ret;
int flags = fcntl(fd, F_GETFL);
if (flags == -1)
ERR_EXIT("fcntl error");
flags |= O_NONBLOCK;
ret = fcntl(fd, F_SETFL, flags);
if (ret == -1)
ERR_EXIT("fcntl error");
}
/* deactivate_nonblock - 设置IO为阻塞模式
* fd: 文件描述符
*/
void deactivate_nonblock(int fd)
{
int ret;
int flags = fcntl(fd, F_GETFL);
if (flags == -1)
ERR_EXIT("fcntl error");
flags &= ~O_NONBLOCK;
ret = fcntl(fd, F_SETFL, flags);
if (ret == -1)
ERR_EXIT("fcntl error");
}
/* connect_timeout - 带超时的connect
* fd: 套接字
* addr: 输出参数,返回对方地址
* wait_seconds: 等待超时秒数,如果为0表示正常模式
* 成功(未超时)返回0,失败返回-1,超时返回-1并且errno = ETIMEDOUT
*/
int connect_timeout(int fd, struct sockaddr_in *addr, unsigned int wait_seconds)
{
int ret;
socklen_t addrlen = sizeof(struct sockaddr_in);
if (wait_seconds > 0)
activate_nonblock(fd);
ret = connect(fd, (struct sockaddr *)addr, addrlen);
if (ret < 0 && errno == EINPROGRESS)
{
fd_set connect_fdset;
struct timeval timeout;
FD_ZERO(&connect_fdset);
FD_SET(fd, &connect_fdset);
timeout.tv_sec = wait_seconds;
timeout.tv_usec = 0;
do
{
/* 一旦连接建立,套接字就可写 */
ret = select(fd + 1, NULL, &connect_fdset, NULL, &timeout);
}
while (ret < 0 && errno == EINTR);
if (ret == 0)
{
errno = ETIMEDOUT;
return -1;
}
else if (ret < 0)
return -1;
else if (ret == 1)
{
/* ret返回为1,可能有两种情况,一种是连接建立成功,一种是套接字产生错误
* 此时错误信息不会保存至errno变量中(select没出错),因此,需要调用
* getsockopt来获取 */
int err;
socklen_t socklen = sizeof(err);
int sockoptret = getsockopt(fd, SOL_SOCKET, SO_ERROR, &err, &socklen);
if (sockoptret == -1)
return -1;
if (err == 0)
ret = 0;
else
{
errno = err;
ret = -1;
}
}
}
if (wait_seconds > 0)
deactivate_nonblock(fd);
return ret;
}
在调用connect前需要使用fcntl函数将套接字标志设置为非阻塞
,如果网络环境很好
,则connect立即返回0,不进入if大括号执行;如果网络环境拥塞
,则connect返回-1且errno ==
EINPROGRESS,表示正在处理。
还需要注意的是当select返回1,可能有两种情况:一种是连接成功,一种是套接字产生错误。
这两种情况都会产生可写事件,所以需要使用getsockopt来获取
一下。
在退出之前还需重新将套接字设置为阻塞
。
connect_timeout函数测试程序
#include "sysutil.h"
int main(void)
{
int sock;
if ((sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
ERR_EXIT("socket");
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(5188);
servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");
int ret = connect_timeout(sock, &servaddr, 5);
if (ret == -1 && errno == ETIMEDOUT)
{
printf("timeout...\n");
return 1;
}
else if (ret == -1)
ERR_EXIT("connect_timeout");
struct sockaddr_in localaddr;
socklen_t addrlen = sizeof(localaddr);
if (getsockname(sock, (struct sockaddr *)&localaddr, &addrlen) < 0)
ERR_EXIT("getsockname");
printf("ip=%s port=%d\n", inet_ntoa(localaddr.sin_addr), ntohs(localaddr.sin_port));
return 0;
}
read_timeout函数的使用
int ret;
ret = read_timeout(fd, 5);
if (ret == 0)
read(fd, buf, sizeof(buf));
else if (ret == -1 && errno == ETIMEOUT)
printf("timeout...\n");
else
ERR_EXIT("read_timeout");
tcp connect机制
connect只是完成发送syn的过程
,后续的两次握手由协议栈成。如果fd是阻塞的,则connect会一直等到超时或者连接成功返回;如果fd是非阻塞的,则connect会立刻返回,但此时协议栈是否已经完成连接要判断下返回值和errno;如果是非阻塞方式,判断返回值;如果是阻塞方式,可以用setsockopt设置SO_SNDTIMEO。